using System;
using System.Collections.Generic;
using System.Text;
using Attach.Internal;
using System.Reflection;
using Xunit;

namespace Test
{
    public class InterceptorTests
    {
        private TestClass obj;
        private TestBehavior behavior;
        private Behaviors behaviors;

        public InterceptorTests()
        {
            obj = Interceptor.GenerateObject<TestClass>();
            behavior = new TestBehavior();
            TestBehavior.RunSequence = "";
            behaviors = new Behaviors();
        }

        private void Attach(string methodName) { Attach(methodName, behavior); }
        private void Attach(string methodName, object behavior) { Attach(methodName, behavior, obj); }
        private void Attach(string methodName, object behavior, object obj)
        {
            behaviors.Add(obj.GetType().GetMethod(methodName), (IBehavior)behavior);
            obj.GetType().GetMethod("SetupBehaviors", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(obj, new object[] { behaviors });
        }

        private void Attach(MethodInfo method, object behavior, object obj)
        {
            behaviors.Add(method, (IBehavior)behavior);
            obj.GetType().GetMethod("SetupBehaviors", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(obj, new object[] { behaviors });
        }

        [Fact]
        public void MethodWithNoReturnTypeAndNoParameters()
        {
            Attach("NoParametersNoReturnValue");

            obj.NoParametersNoReturnValue();

            Assert.True(obj.WasCalled);
            Assert.True(behavior.WasCalled);
        }

        [Fact]
        public void MethodWithReferenceTypeReturnValue()
        {
            Attach("NoParametersStringReturnValue");

            string result = obj.NoParametersStringReturnValue();

            Assert.Equal("success", result);
            Assert.True(obj.WasCalled);
            Assert.True(behavior.WasCalled);
        }

        [Fact]
        public void MethodWithValueTypeReturnValue()
        {
            Attach("NoParametersIntReturnValue");

            int result = obj.NoParametersIntReturnValue();

            Assert.Equal(123, result);
            Assert.True(obj.WasCalled);
            Assert.True(behavior.WasCalled);
        }

        [Fact]
        public void MethodWithValueTypeParameterNoReturnValue()
        {
            Attach("IntParameterNoReturnValue");

            obj.IntParameterNoReturnValue(123);

            Assert.Equal(123, obj.Param1);
            Assert.Equal(123, behavior.Parameters[0]);
            Assert.True(obj.WasCalled);
            Assert.True(behavior.WasCalled);
        }

        [Fact]
        public void MethodWithReferenceTypeParameterNoReturnValue()
        {
            Attach("StringParameterNoReturnValue");

            obj.StringParameterNoReturnValue("123");

            Assert.Equal("123", obj.Param1);
            Assert.Equal("123", behavior.Parameters[0]);
            Assert.True(obj.WasCalled);
            Assert.True(behavior.WasCalled);
        }

        [Fact]
        public void MethodWithTwoParameters()
        {
            Attach("TwoParametersNoReturnValue");

            obj.TwoParametersNoReturnValue("123", "456");

            Assert.Equal("123", obj.Param1);
            Assert.Equal("456", obj.Param2);
            Assert.Equal("123", behavior.Parameters[0]);
            Assert.Equal("456", behavior.Parameters[1]);
            Assert.True(obj.WasCalled);
            Assert.True(behavior.WasCalled);
        }

        [Fact]
        public void MethodWithTwelveParameters()
        {
            Attach("TwelveParametersNoReturnValue");

            obj.TwelveParametersNoReturnValue("1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12");

            Assert.Equal("1", obj.Param1);
            Assert.Equal("2", obj.Param2);
            Assert.Equal("3", obj.Param3);
            Assert.Equal("4", obj.Param4);
            Assert.Equal("5", obj.Param5);
            Assert.Equal("6", obj.Param6);
            Assert.Equal("7", obj.Param7);
            Assert.Equal("8", obj.Param8);
            Assert.Equal("9", obj.Param9);
            Assert.Equal("10", obj.Param10);
            Assert.Equal("11", obj.Param11);
            Assert.Equal("12", obj.Param12);
            Assert.Equal("1", behavior.Parameters[0]);
            Assert.Equal("2", behavior.Parameters[1]);
            Assert.Equal("3", behavior.Parameters[2]);
            Assert.Equal("4", behavior.Parameters[3]);
            Assert.Equal("5", behavior.Parameters[4]);
            Assert.Equal("6", behavior.Parameters[5]);
            Assert.Equal("7", behavior.Parameters[6]);
            Assert.Equal("8", behavior.Parameters[7]);
            Assert.Equal("10", behavior.Parameters[9]);
            Assert.Equal("11", behavior.Parameters[10]);
            Assert.Equal("12", behavior.Parameters[11]);
            Assert.True(obj.WasCalled);
            Assert.True(behavior.WasCalled);
        }

        [Fact]
        public void MethodWithBothReferenceAndValueTypeParameters()
        {
            Attach("IntAndStringParametersNoReturnValue");

            obj.IntAndStringParametersNoReturnValue(123, "456");

            Assert.Equal(123, obj.Param1);
            Assert.Equal("456", obj.Param2);
            Assert.Equal(123, behavior.Parameters[0]);
            Assert.Equal("456", behavior.Parameters[1]);
            Assert.True(obj.WasCalled);
            Assert.True(behavior.WasCalled);
        }

        [Fact]
        public void MethodWithParameterAndReturnValue()
        {
            Attach("StringParameterStringReturnValue");

            string result = obj.StringParameterStringReturnValue("123");

            Assert.Equal("success", result);
            Assert.Equal("123", obj.Param1);
            Assert.Equal("123", behavior.Parameters[0]);
            Assert.True(obj.WasCalled);
            Assert.True(behavior.WasCalled);
        }

        [Fact]
        public void MethodRunsWithNoBehaviorsAttached()
        {
            obj.NoParametersNoReturnValue();

            Assert.True(obj.WasCalled);
            Assert.False(behavior.WasCalled);
        }

        [Fact]
        public void AllBehaviorsRunWhenMultipleBehaviorsAttached()
        {
            TestBehavior behavior1 = new TestBehavior();
            TestBehavior behavior2 = new TestBehavior();
            TestBehavior behavior3 = new TestBehavior();
            Attach("NoParametersNoReturnValue", behavior1);
            Attach("NoParametersNoReturnValue", behavior2);
            Attach("NoParametersNoReturnValue", behavior3);

            obj.NoParametersNoReturnValue();

            Assert.True(obj.WasCalled);
            Assert.True(behavior1.WasCalled);
            Assert.True(behavior2.WasCalled);
            Assert.True(behavior3.WasCalled);
        }

        [Fact]
        public void BehaviorsRunInCorrectOrderWhenMultipleBehaviorsAttached()
        {
            TestBehavior behavior1 = new TestBehavior(1);
            TestBehavior behavior2 = new TestBehavior(2);
            TestBehavior behavior3 = new TestBehavior(3);
            Attach("NoParametersNoReturnValue", behavior1);
            Attach("NoParametersNoReturnValue", behavior2);
            Attach("NoParametersNoReturnValue", behavior3);

            obj.NoParametersNoReturnValue();

            Assert.True(obj.WasCalled);
            Assert.Equal("123", TestBehavior.RunSequence);
        }

        [Fact]
        public void MethodsAndBehaviorsRunWhenClassHasMultipleMethodsAttached()
        {
            TestClass2 obj = Interceptor.GenerateObject<TestClass2>();
            TestBehavior behavior1 = new TestBehavior();
            TestBehavior behavior2 = new TestBehavior();
            Attach("TestMethod1", behavior1, obj);
            Attach("TestMethod2", behavior2, obj);

            obj.TestMethod1();
            obj.TestMethod2();

            Assert.True(obj.Was1Called);
            Assert.True(obj.Was2Called);
            Assert.True(behavior1.WasCalled);
            Assert.True(behavior2.WasCalled);
        }

        [Fact]
        public void GenerateObject_ConstructorHavingParameter()
        {
            TestClass obj = Interceptor.GenerateObject<TestClass>("123");
            Attach("NoParametersNoReturnValue", behavior, obj);

            obj.NoParametersNoReturnValue();

            Assert.Equal("123", obj.Param1);
            Assert.True(obj.WasCalled);
            Assert.True(behavior.WasCalled);
        }

        [Fact]
        public void GenerateObject_ConstructorHavingFiveParameters()
        {
            TestClass obj = Interceptor.GenerateObject<TestClass>("1","2","3","4","5");
            Attach("NoParametersNoReturnValue", behavior, obj);

            obj.NoParametersNoReturnValue();

            Assert.Equal("1", obj.Param1);
            Assert.Equal("2", obj.Param2);
            Assert.Equal("3", obj.Param3);
            Assert.Equal("4", obj.Param4);
            Assert.Equal("5", obj.Param5);
            Assert.True(obj.WasCalled);
            Assert.True(behavior.WasCalled);
        }

        [Fact]
        public void GenerateObjectWorksWithOverloadedMethod()
        {
            Attach(typeof(TestClass).GetMethod("OverloadedMethod", new Type[] { typeof(string) }), behavior, obj);

            obj.OverloadedMethod("123");

            Assert.Equal("123", obj.Param1);
            Assert.True(behavior.WasCalled);
        }

        //[Test, ExpectedException(typeof(Exception), "Cannot add behavior to multiple overloads")]
        //public void TestAttachFailsWhenAttachingToMultipleOverloadedMethods()
        //{
        //    Interceptor<GenerateObjectWorksWithOverloadedMethod> interceptor = new Interceptor<GenerateObjectWorksWithOverloadedMethod>();
        //    //interceptor.Attach("TestMethod", new Type[] { }, new TestBehavior());
        //    //interceptor.Attach("TestMethod", new Type[] { typeof(string) }, new TestBehavior());
        //}

        //[Test, ExpectedException(typeof(Exception), "Method specified is overloaded, use types parameter")]
        //public void TestAttachFailsWithOverloadedMethodWhenTypesParameterNotSpecified()
        //{
        //    Interceptor<GenerateObjectWorksWithOverloadedMethod> interceptor = new Interceptor<GenerateObjectWorksWithOverloadedMethod>();
        //    //interceptor.Attach("TestMethod", new TestBehavior());
        //}

        //public class InterceptorThrowsErrorIfAttachToNonExistentMethod
        //{
        //}

        //[Test, ExpectedException(typeof(ArgumentException), "Invalid or non-public method name")]
        //public void TestInterceptorThrowsErrorIfAttachToNonExistentMethod()
        //{
        //    Interceptor<InterceptorThrowsErrorIfAttachToNonExistentMethod> intercepter = new Interceptor<InterceptorThrowsErrorIfAttachToNonExistentMethod>();
        //    //intercepter.Attach("invalidmethod", new TestBehavior());
        //}

        //public class InterceptorThrowsErrorIfAttachToNonVirtual
        //{
        //    public void InvalidMethod() { }
        //}

        //[Test, ExpectedException(typeof(ArgumentException), "Invalid method name InvalidMethod, method not virtual")]
        //public void TestInterceptorThrowsErrorIfAttachToNonVirtualMethod()
        //{
        //    Interceptor<InterceptorThrowsErrorIfAttachToNonVirtual> intercepter = new Interceptor<InterceptorThrowsErrorIfAttachToNonVirtual>();
        //    //intercepter.Attach("InvalidMethod", new TestBehavior());
        //}

        //public interface IInterceptorThrowsErrorIfAttachToFinal
        //{
        //    void InvalidMethod();
        //}

        //public class InterceptorThrowsErrorIfAttachToFinal : IInterceptorThrowsErrorIfAttachToFinal
        //{
        //    public void InvalidMethod() { }
        //}

        //[Test, ExpectedException(typeof(ArgumentException), "Invalid method name InvalidMethod, method not virtual")]
        //public void TestInterceptorThrowsErrorIfAttachToFinal()
        //{
        //    Interceptor<InterceptorThrowsErrorIfAttachToFinal> intercepter = new Interceptor<InterceptorThrowsErrorIfAttachToFinal>();
        //    //intercepter.Attach("InvalidMethod", new TestBehavior());
        //}

        [Fact]
        public void GenerateObject_ClassWithNonVirtualInterfaceMethods()
        {
            TestClass obj = Interceptor.GenerateObject<TestClass>();
        }

        //public class InterceptorThrowsErrorIfAttachToNonPublicMethod
        //{
        //    protected virtual void InvalidMethod() { System.Diagnostics.Debug.WriteLine("whee"); }
        //}

        //[Test, ExpectedException(typeof(ArgumentException), "Invalid or non-public method name")]
        //public void TestInterceptorThrowsErrorIfAttachToNonPublicMethod()
        //{
        //    Interceptor<InterceptorThrowsErrorIfAttachToNonPublicMethod> intercepter = new Interceptor<InterceptorThrowsErrorIfAttachToNonPublicMethod>();
        //    //intercepter.Attach("InvalidMethod", new TestBehavior());
        //}

        //public class InterceptorThrowsErrorIfAttachToStaticMethod
        //{
        //    public static void InvalidMethod() { }
        //}

        //[Test, ExpectedException(typeof(ArgumentException), "Invalid method name InvalidMethod, method not virtual")]
        //public void TestInterceptorThrowsErrorIfAttachToStaticMethod()
        //{
        //    Interceptor<InterceptorThrowsErrorIfAttachToStaticMethod> intercepter = new Interceptor<InterceptorThrowsErrorIfAttachToStaticMethod>();
        //    //intercepter.Attach("InvalidMethod", new TestBehavior());
        //}

        [Fact]
        public void GenerateObject_ConstructorWithSingleNullParameter()
        {
            TestClass obj = Interceptor.GenerateObject<TestClass>(null);
        }

        [Fact]
        public void GenerateObject_ConstructorWithMultipleNullParameters()
        {
            TestClass obj = Interceptor.GenerateObject<TestClass>(null, null, null);
        }

        [Fact]
        public void GenerateObject_MultipleConstructorsHavingSameParameterCountAndUsingNullParameters_ThrowsException()
        {
            Exception result = Record.Exception(delegate { Interceptor.GenerateObject<TestClass5>(null); });

            Assert.NotNull(result);
            Assert.Equal("Multiple constructors match parameter count, cannot determine correct one because of null parameter", result.Message);
        }

        public class TestBehavior : IBehavior
        {
            public static string RunSequence;

            protected ContinueMethod continueMethod;
            public object[] Parameters = null;
            public bool WasCalled = false;
            public int Index = 0;

            public TestBehavior() { }
            public TestBehavior(int index)
            {
                Index = index;
            }

            public ContinueMethod Continue
            {
                set { continueMethod = value; }
            }

            public virtual object Run(object[] parameters)
            {
                RunSequence += Index;
                Parameters = parameters;
                WasCalled = true;
                return continueMethod(parameters);
            }
        }

        public interface Interface1
        {
            void InterfaceMethod();
        }

        public class TestClass : Interface1
        {
            public bool WasCalled = false;
            public object Param1 = null;
            public object Param2 = null;
            public object Param3 = null;
            public object Param4 = null;
            public object Param5 = null;
            public object Param6 = null;
            public object Param7 = null;
            public object Param8 = null;
            public object Param9 = null;
            public object Param10 = null;
            public object Param11 = null;
            public object Param12 = null;

            public TestClass() { }
            public TestClass(string p1) { Param1 = p1; }
            public TestClass(string p1, string p2, string p3) { Param1 = p1; Param2 = p2; Param3 = p3; }
            public TestClass(string p1, string p2, string p3, string p4, string p5) { Param1 = p1; Param2 = p2; Param3 = p3; Param4 = p4; Param5 = p5; }

            public virtual void NoParametersNoReturnValue() { WasCalled = true; }
            public virtual string NoParametersStringReturnValue() { WasCalled = true; return "success"; }
            public virtual int NoParametersIntReturnValue() { WasCalled = true; return 123; }
            public virtual void IntParameterNoReturnValue(int p1) { WasCalled = true; Param1 = p1; }
            public virtual void StringParameterNoReturnValue(string p1) { WasCalled = true; Param1 = p1; }
            public virtual void TwoParametersNoReturnValue(string p1, string p2) { WasCalled = true; Param1 = p1; Param2 = p2; }
            public virtual void TwelveParametersNoReturnValue(string p1, string p2, string p3, string p4, string p5, string p6, string p7, string p8, string p9, string p10, string p11, string p12) { WasCalled = true; Param1 = p1; Param2 = p2; Param3 = p3; Param4 = p4; Param5 = p5; Param6 = p6; Param7 = p7; Param8 = p8; Param9 = p9; Param10 = p10; Param11 = p11; Param12 = p12; }
            public virtual void IntAndStringParametersNoReturnValue(int p1, string p2) { WasCalled = true; Param1 = p1; Param2 = p2; }
            public virtual string StringParameterStringReturnValue(string p1) { WasCalled = true; Param1 = p1; return "success"; }
            public virtual void OverloadedMethod() { WasCalled = true; }
            public virtual void OverloadedMethod(string p1) { WasCalled = true; Param1 = p1; }
            public void InterfaceMethod() { WasCalled = true; }
        }

        public class TestClass2
        {
            public bool Was1Called = false;
            public bool Was2Called = false;
            public virtual void TestMethod1() { Was1Called = true; }
            public virtual void TestMethod2() { Was2Called = true; }
        }

        public class TestClass5
        {
            public TestClass5(string one) { }
            public TestClass5(object one) { }
        }
    }
}